import {
    extend,
    bind,
    on,
    off,
    getAttr,
    getBindAttr,
    camelize,
    nextTick,
    warn
}
from './util/index'
import Watcher from './watcher'
import {
    parseExpression,
    isSimplePath
}
from './parsers/expression'

function noop() {}

/**
 * A directive links a DOM element with a piece of data,
 * which is the result of evaluating an expression.
 * It registers a watcher with the expression and calls
 * the DOM update function when a change is triggered.
 *
 * @param {String} name
 * @param {Node} el
 * @param {Vue} vm
 * @param {Object} descriptor
 *                 - {String} name
 *                 - {Object} def
 *                 - {String} expression
 *                 - {Array<Object>} [filters]
 *                 - {Boolean} literal
 *                 - {String} attr
 *                 - {String} raw
 * @param {Object} def - directive definition object
 * @param {Vue} [host] - transclusion host component
 * @param {Object} [scope] - v-for scope
 * @param {Fragment} [frag] - owner fragment
 * @constructor
 */

export
default

function Directive(descriptor, vm, el, host, scope, frag) {
    this.vm = vm
    this.el = el
        // copy descriptor properties
    this.descriptor = descriptor
    this.name = descriptor.name
    this.expression = descriptor.expression
    this.arg = descriptor.arg
    this.modifiers = descriptor.modifiers
    this.filters = descriptor.filters
    this.literal = this.modifiers && this.modifiers.literal
        // private
    this._locked = false
    this._bound = false
    this._listeners = null
        // link context
    this._host = host
    this._scope = scope
    this._frag = frag
        // store directives on node in dev mode
    if (process.env.NODE_ENV !== 'production' && this.el) {
        this.el._vue_directives = this.el._vue_directives || []
        this.el._vue_directives.push(this)
    }
}

/**
 * Initialize the directive, mixin definition properties,
 * setup the watcher, call definition bind() and update()
 * if present.
 *
 * @param {Object} def
 */

Directive.prototype._bind = function() {
    var name = this.name
    var descriptor = this.descriptor

    // remove attribute
    if (
        (name !== 'cloak' || this.vm._isCompiled) &&
        this.el && this.el.removeAttribute
    ) {
        var attr = descriptor.attr || ('v-' + name)
        this.el.removeAttribute(attr)
    }

    // copy def properties
    var def = descriptor.def
    if (typeof def === 'function') {
        this.update = def
    } else {
        extend(this, def)
    }

    // setup directive params
    this._setupParams()

    // initial bind
    if (this.bind) {
        this.bind()
    }
    this._bound = true

    if (this.literal) {
        this.update && this.update(descriptor.raw)
    } else if (
        (this.expression || this.modifiers) &&
        (this.update || this.twoWay) && !this._checkStatement()
    ) {
        // wrapped updater for context
        var dir = this
        if (this.update) {
            this._update = function(val, oldVal) {
                if (!dir._locked) {
                    dir.update(val, oldVal)
                }
            }
        } else {
            this._update = noop
        }
        var preProcess = this._preProcess ? bind(this._preProcess, this) : null
        var postProcess = this._postProcess ? bind(this._postProcess, this) : null
        var watcher = this._watcher = new Watcher(
                this.vm,
                this.expression,
                this._update, // callback
                {
                    filters: this.filters,
                    twoWay: this.twoWay,
                    deep: this.deep,
                    preProcess: preProcess,
                    postProcess: postProcess,
                    scope: this._scope
                }
            )
            // v-model with inital inline value need to sync back to
            // model instead of update to DOM on init. They would
            // set the afterBind hook to indicate that.
        if (this.afterBind) {
            this.afterBind()
        } else if (this.update) {
            this.update(watcher.value)
        }
    }
}

/**
 * Setup all param attributes, e.g. track-by,
 * transition-mode, etc...
 */

Directive.prototype._setupParams = function() {
    if (!this.params) {
        return
    }
    var params = this.params
        // swap the params array with a fresh object.
    this.params = Object.create(null)
    var i = params.length
    var key, val, mappedKey
    while (i--) {
        key = params[i]
        mappedKey = camelize(key)
        val = getBindAttr(this.el, key)
        if (val != null) {
            // dynamic
            this._setupParamWatcher(mappedKey, val)
        } else {
            // static
            val = getAttr(this.el, key)
            if (val != null) {
                this.params[mappedKey] = val === '' ? true : val
            }
        }
    }
}

/**
 * Setup a watcher for a dynamic param.
 *
 * @param {String} key
 * @param {String} expression
 */

Directive.prototype._setupParamWatcher = function(key, expression) {
    var self = this
    var called = false
    var unwatch = (this._scope || this.vm).$watch(expression, function(val, oldVal) {
        self.params[key] = val
            // since we are in immediate mode,
            // only call the param change callbacks if this is not the first update.
        if (called) {
            var cb = self.paramWatchers && self.paramWatchers[key]
            if (cb) {
                cb.call(self, val, oldVal)
            }
        } else {
            called = true
        }
    }, {
        immediate: true,
        user: false
    });
    (this._paramUnwatchFns || (this._paramUnwatchFns = [])).push(unwatch)
}

/**
 * Check if the directive is a function caller
 * and if the expression is a callable one. If both true,
 * we wrap up the expression and use it as the event
 * handler.
 *
 * e.g. on-click="a++"
 *
 * @return {Boolean}
 */

Directive.prototype._checkStatement = function() {
    var expression = this.expression
    if (
        expression && this.acceptStatement && !isSimplePath(expression)
    ) {
        var fn = parseExpression(expression).get
        var scope = this._scope || this.vm
        var handler = function(e) {
            scope.$event = e
            fn.call(scope, scope)
            scope.$event = null
        }
        if (this.filters) {
            handler = scope._applyFilters(handler, null, this.filters)
        }
        this.update(handler)
        return true
    }
}

/**
 * Set the corresponding value with the setter.
 * This should only be used in two-way directives
 * e.g. v-model.
 *
 * @param {*} value
 * @public
 */

Directive.prototype.set = function(value) {
    /* istanbul ignore else */
    if (this.twoWay) {
        this._withLock(function() {
            this._watcher.set(value)
        })
    } else if (process.env.NODE_ENV !== 'production') {
        warn(
            'Directive.set() can only be used inside twoWay' +
            'directives.'
        )
    }
}

/**
 * Execute a function while preventing that function from
 * triggering updates on this directive instance.
 *
 * @param {Function} fn
 */

Directive.prototype._withLock = function(fn) {
    var self = this
    self._locked = true
    fn.call(self)
    nextTick(function() {
        self._locked = false
    })
}

/**
 * Convenience method that attaches a DOM event listener
 * to the directive element and autometically tears it down
 * during unbind.
 *
 * @param {String} event
 * @param {Function} handler
 * @param {Boolean} [useCapture]
 */

Directive.prototype.on = function(event, handler, useCapture) {
    on(this.el, event, handler, useCapture);
    (this._listeners || (this._listeners = []))
    .push([event, handler])
}

/**
 * Teardown the watcher and call unbind.
 */

Directive.prototype._teardown = function() {
    if (this._bound) {
        this._bound = false
        if (this.unbind) {
            this.unbind()
        }
        if (this._watcher) {
            this._watcher.teardown()
        }
        var listeners = this._listeners
        var i
        if (listeners) {
            i = listeners.length
            while (i--) {
                off(this.el, listeners[i][0], listeners[i][1])
            }
        }
        var unwatchFns = this._paramUnwatchFns
        if (unwatchFns) {
            i = unwatchFns.length
            while (i--) {
                unwatchFns[i]()
            }
        }
        if (process.env.NODE_ENV !== 'production' && this.el) {
            this.el._vue_directives.$remove(this)
        }
        this.vm = this.el = this._watcher = this._listeners = null
    }
}
